import pandas as pd
import openpyxl
import numpy as np
import os
from typing import Dict, List, Tuple, NamedTuple
import tkinter as tk
from tkinter import filedialog
from tkinter import messagebox
import configparser
from datetime import datetime, timedelta
import random
from license_manager import LicenseManager
from license_ui import ActivationDialog


class SheetScore(NamedTuple):
    """单个sheet页的评分结果"""
    sheet_name: str
    score: float
    deductions: Dict


class ExcelScorer:
    def __init__(self):
        self.standard_file = None
        self.scoring_file = None
        self.submissions_dir = None
        self.output_dir = None

    def load_scoring_template(self, file_path: str) -> Dict[str, pd.DataFrame]:
        """
        加载评分模板，返回每个sheet页的评分矩阵
        返回格式: {sheet_name: scoring_df}
        """
        wb = openpyxl.load_workbook(file_path)
        scoring_sheets = {}

        # 遍历每个sheet页
        for sheet_name in wb.sheetnames:
            sheet = wb[sheet_name]
            # 创建与sheet大小相同的零矩阵
            scoring_matrix = np.zeros((sheet.max_row, sheet.max_column))

            # 填充分值
            for row in range(1, sheet.max_row + 1):
                for col in range(1, sheet.max_column + 1):
                    cell = sheet.cell(row=row, column=col)
                    # 检查单元格是否有背景色
                    if cell.fill.start_color.index != '00000000':  # 非白色背景
                        try:
                            score = float(cell.value)
                            scoring_matrix[row - 1][col - 1] = score
                        except (ValueError, TypeError):
                            continue

            # 转换为DataFrame
            scoring_df = pd.DataFrame(scoring_matrix)
            if scoring_df.values.sum() > 0:  # 只保存有分值的sheet
                scoring_sheets[sheet_name] = scoring_df

        return scoring_sheets

    def compare_sheets(self,
                       standard_df: pd.DataFrame,
                       submission_df: pd.DataFrame) -> pd.DataFrame:
        """
        比较两个DataFrame，返回差异矩阵（相同为0，不同为1）
        """
        # 确保两个DataFrame大小一致
        max_rows = max(len(standard_df), len(submission_df))
        max_cols = max(len(standard_df.columns), len(submission_df.columns))

        # 创建空的差异矩阵
        diff_matrix = pd.DataFrame(0, index=range(max_rows), columns=range(max_cols), dtype=int)

        # 比较每个单元格
        for i in range(max_rows):
            for j in range(max_cols):
                # 获取标准答案的值
                std_val = standard_df.iloc[i, j] if i < len(standard_df) and j < len(standard_df.columns) else 0
                # 获取提交作业的值
                sub_val = submission_df.iloc[i, j] if i < len(submission_df) and j < len(submission_df.columns) else 0

                # 处理空值
                if pd.isna(std_val):
                    std_val = 0
                if pd.isna(sub_val):
                    sub_val = 0

                try:
                    # 尝试数值比较
                    std_num = float(std_val) if str(std_val).strip() != '' else 0
                    sub_num = float(sub_val) if str(sub_val).strip() != '' else 0
                    if abs(std_num - sub_num) >= 0.01:
                        diff_matrix.iloc[i, j] = 1
                except (ValueError, TypeError):
                    # 字符串比较
                    if str(std_val).strip() != str(sub_val).strip():
                        diff_matrix.iloc[i, j] = 1

        return diff_matrix

    def calculate_sheet_score(self,
                              standard_df: pd.DataFrame,
                              submission_df: pd.DataFrame,
                              scoring_df: pd.DataFrame) -> Tuple[float, Dict]:
        """
        计算单个sheet的得分
        """
        # 获取差异矩阵
        diff_matrix = self.compare_sheets(standard_df, submission_df)

        # 计算扣分（差异矩阵与评分矩阵相乘）
        deduction_matrix = diff_matrix * scoring_df
        total_deduction = deduction_matrix.sum().sum()

        # 收集扣分详情
        deductions = {}
        for i in range(len(diff_matrix)):
            for j in range(len(diff_matrix.columns)):
                if diff_matrix.iloc[i, j] == 1:  # 如果有差异
                    # 获取标准答案的值（如果存在）
                    expected = standard_df.iloc[i, j] if i < len(standard_df) and j < len(standard_df.columns) else None

                    # 获取提交作业的值（如果存在）
                    submitted = submission_df.iloc[i, j] if i < len(submission_df) and j < len(
                        submission_df.columns) else None

                    # 获取扣分值（如果存在）
                    points = scoring_df.iloc[i, j] if i < len(scoring_df) and j < len(scoring_df.columns) else 0

                    deductions[(i, j)] = {
                        'expected': expected if expected is not None else '未填写',
                        'submitted': submitted if submitted is not None else '未填写',
                        'points_deducted': points
                    }

        return total_deduction, deductions

    def process_single_submission(self,
                                  submission_path: str,
                                  standard_dfs: Dict[str, pd.DataFrame],
                                  scoring_sheets: Dict[str, pd.DataFrame]) -> List[SheetScore]:
        """处理单个提交的作业文件"""
        sheet_scores = []
        # 读取提交的作业，不使用第一行作为表头
        submission_dfs = pd.read_excel(submission_path, sheet_name=None, header=None)

        # 处理每个需要评分的sheet
        for sheet_name, scoring_df in scoring_sheets.items():
            if sheet_name not in submission_dfs:
                print(f"警告：{os.path.basename(submission_path)}中缺少sheet页：{sheet_name}")
                # 缺失sheet页记为扣100分
                sheet_scores.append(SheetScore(
                    sheet_name=sheet_name,
                    score=100,  # 扣100分
                    deductions={(0, 0): {
                        'expected': 'Sheet存在',
                        'submitted': 'Sheet缺失',
                        'points_deducted': 100
                    }}
                ))
                continue

            if sheet_name not in standard_dfs:
                print(f"警告：标准答案中缺少sheet页：{sheet_name}")
                continue

            score, deductions = self.calculate_sheet_score(
                standard_dfs[sheet_name],
                submission_dfs[sheet_name],
                scoring_df
            )

            sheet_scores.append(SheetScore(sheet_name, score, deductions))

        return sheet_scores

    def process_submissions(self, total_score: float = 100):
        """处理所有提交的作业"""
        # 加载标准答案（所有sheet页），不使用第一行作为表头
        standard_dfs = pd.read_excel(self.standard_file, sheet_name=None, header=None)

        # 加载评分模板
        scoring_sheets = self.load_scoring_template(self.scoring_file)

        # 准备结果数据
        results = []

        # 处理每个提交的作业
        for submission_file in os.listdir(self.submissions_dir):
            if not submission_file.endswith(('.xlsx', '.xls')):
                continue

            submission_path = os.path.join(self.submissions_dir, submission_file)
            if submission_path == self.standard_file or submission_path == self.scoring_file:
                continue
            print(f"\n正在评分: {submission_file}")

            try:
                # 评分
                sheet_scores = self.process_single_submission(
                    submission_path, standard_dfs, scoring_sheets
                )

                # 记录结果
                result = {
                    'student_file': submission_file,
                    'sheet_scores': sheet_scores
                }
                results.append(result)
            except Exception as e:
                print(f"处理文件 {submission_file} 时出错: {str(e)}")

        # 生成评分报告
        self.generate_report(results, scoring_sheets.keys(), total_score)

    def generate_report(self, results: List[Dict], sheet_names: List[str], total_score: float = 100):
        """生成评分报告"""
        # 获取标准答案文件名（不包含扩展名）
        standard_base_name = os.path.splitext(os.path.basename(self.standard_file))[0]

        # 创建基础评分数据（汇总文件）
        basic_data = []
        for r in results:
            row_data = {
                '文件名': r['student_file'],
                '总扣分': 0
            }

            # 初始化所有sheet的扣分为0
            for sheet_name in sheet_names:
                row_data[f'{sheet_name}扣分'] = 0

            # 添加每个sheet的扣分
            for sheet_score in r['sheet_scores']:
                sheet_name = sheet_score.sheet_name
                deduction = sheet_score.score
                row_data[f'{sheet_name}扣分'] = deduction
                row_data['总扣分'] += deduction

            # 计算最终得分
            row_data['最终得分'] = max(0, total_score - row_data['总扣分'])
            basic_data.append(row_data)

        # 创建汇总报告
        df_basic = pd.DataFrame(basic_data)
        columns = ['文件名'] + [f'{sheet}扣分' for sheet in sheet_names] + ['总扣分', '最终得分']
        df_basic = df_basic[columns]

        # 按最终得分降序排序
        df_basic = df_basic.sort_values('最终得分', ascending=False)

        # 保存汇总报告
        output_file = os.path.join(self.output_dir, f'{standard_base_name}_汇总结果.xlsx')
        df_basic.to_excel(output_file, index=False)

        # 为每个sheet创建详细的评分文件
        for sheet_name in sheet_names:
            sheet_details = []

            for r in results:
                student_file = r['student_file']
                sheet_deductions = []

                # 找到当前sheet的评分结果
                for sheet_score in r['sheet_scores']:
                    if sheet_score.sheet_name == sheet_name:
                        # 收集所有扣分项
                        for (row, col), deduction in sheet_score.deductions.items():
                            sheet_deductions.append({
                                '单元格': f'R{row + 1}C{col + 1}' if row != 0 or col != 0 else 'Sheet缺失',
                                '预期值': deduction['expected'],
                                '实际值': deduction['submitted'],
                                '扣分': deduction['points_deducted']
                            })

                if sheet_deductions:
                    # 计算该学生在这个sheet的总扣分
                    total_deduction = sum(d['扣分'] for d in sheet_deductions)

                    # 添加到详细信息中
                    sheet_details.append({
                        'student_file': student_file,
                        'total_deduction': total_deduction,
                        'deduction_details': sheet_deductions
                    })

            if sheet_details:
                # 创建详细报告
                details_file = os.path.join(self.output_dir, f'{standard_base_name}_{sheet_name}_详细.xlsx')
                with pd.ExcelWriter(details_file, engine='openpyxl') as writer:
                    # 保存总体扣分情况
                    summary_data = [{
                        '文件名': detail['student_file'],
                        '总扣分': detail['total_deduction']
                    } for detail in sheet_details]
                    pd.DataFrame(summary_data).to_excel(writer, sheet_name='扣分汇总', index=False)

                    # 保存每个学生的详细扣分情况
                    for detail in sheet_details:
                        student_name = os.path.splitext(detail['student_file'])[0]
                        # 限制sheet名长度（Excel限制）
                        sheet_tab_name = student_name[:31] if len(student_name) > 31 else student_name
                        sheet_tab_name = sheet_tab_name.replace("[", "").replace("]", "").replace("?", "").replace("/", "").replace("\\", "").replace("*", "")

                        df_detail = pd.DataFrame(detail['deduction_details'])
                        df_detail.to_excel(writer, sheet_name=sheet_tab_name, index=False)


def select_file(title: str, filetypes: tuple = (("Excel files", "*.xlsx;*.xls"), ("All files", "*.*"))) -> str:
    """
    打开文件选择对话框
    Args:
        title: 对话框标题
        filetypes: 文件类型过滤
    Returns:
        选择的文件路径，如果取消则返回空字符串
    """
    root = tk.Tk()
    root.withdraw()  # 隐藏主窗口
    file_path = filedialog.askopenfilename(
        title=title,
        filetypes=filetypes
    )
    return file_path


def select_directory(title: str) -> str:
    """
    打开目录选择对话框
    Args:
        title: 对话框标题
    Returns:
        选择的目录路径，如果取消则返回空字符串
    """
    root = tk.Tk()
    root.withdraw()  # 隐藏主窗口
    directory = filedialog.askdirectory(
        title=title
    )
    return directory


def create_guide_window(message: str, detail: str = "") -> tk.Tk:
    """
    创建引导窗口
    Args:
        message: 主要提示信息（步骤标题）
        detail: 详细说明文本
    Returns:
        窗口实例
    """
    window = tk.Tk()
    window.title("操作指引")

    # 设置窗口大小和位置
    window_width = 400
    window_height = 180 if detail else 150
    screen_width = window.winfo_screenwidth()
    screen_height = window.winfo_screenheight()
    x = (screen_width - window_width) // 2
    y = (screen_height - window_height) // 2
    window.geometry(f"{window_width}x{window_height}+{x}+{y}")

    # 添加主要提示文本（步骤标题）
    label_main = tk.Label(
        window,
        text=message,
        wraplength=350,
        justify="center",
        font=("Arial", 12, "bold")  # 使用粗体
    )
    label_main.pack(pady=(20, 0))

    # 添加详细说明文本
    if detail:
        label_detail = tk.Label(
            window,
            text=detail,
            wraplength=350,
            justify="center",
            font=("Arial", 10)  # 较小的字号
        )
        label_detail.pack(pady=(10, 0))

    # 添加确认按钮
    button = tk.Button(
        window,
        text="确定",
        command=window.destroy,
        width=10,
        font=("Arial", 10)
    )
    button.pack(pady=20)

    return window


def select_with_guide(title: str, detail: str, select_func, *args, **kwargs):
    """
    显示引导窗口并执行选择操作
    Args:
        title: 步骤标题
        detail: 详细说明
        select_func: 选择函数（select_file或select_directory）
        args, kwargs: 传递给选择函数的参数
    Returns:
        选择的结果
    """
    guide_window = create_guide_window(title, detail)
    guide_window.wait_window()
    return select_func(*args, **kwargs)


def create_score_input_window() -> float:
    """
    创建输入总分的对话框
    Returns:
        输入的总分，如果取消则返回默认值100
    """
    total_score = 100.0  # 在外层函数中定义 total_score

    def on_ok():
        nonlocal total_score  # 声明使用外层函数的 total_score
        try:
            value = float(entry.get() or "100")
            if value <= 0:
                messagebox.showerror("错误", "总分必须大于0")
                return
            total_score = value
            window.destroy()  # 使用destroy而不是quit
        except ValueError:
            messagebox.showerror("错误", "请输入有效的数字")

    window = tk.Tk()
    window.title("设置总分")

    # 设置窗口大小和位置
    window_width = 300
    window_height = 150
    screen_width = window.winfo_screenwidth()
    screen_height = window.winfo_screenheight()
    x = (screen_width - window_width) // 2
    y = (screen_height - window_height) // 2
    window.geometry(f"{window_width}x{window_height}+{x}+{y}")

    # 添加说明文本
    label = tk.Label(
        window,
        text="请输入总分\n（默认为100分）",
        justify="center",
        font=("Arial", 11)
    )
    label.pack(pady=(20, 10))

    # 添加输入框
    entry = tk.Entry(window, width=10, font=("Arial", 11))
    entry.pack(pady=5)
    entry.insert(0, "100")  # 默认值

    # 添加确定按钮
    button = tk.Button(
        window,
        text="确定",
        command=on_ok,
        width=10,
        font=("Arial", 10)
    )
    button.pack(pady=10)

    # 设置焦点并选中默认值
    entry.focus_set()
    entry.select_range(0, tk.END)

    # 绑定回车键
    window.bind('<Return>', lambda e: on_ok())

    # 设置窗口为模态
    window.grab_set()
    window.wait_window()

    return total_score


def get_initial_special_date() -> str:
    """
    获取初始特殊日期（当前年份的12月31日）
    Returns:
        日期字符串，格式：YYYY-MM-DD
    """
    current_year = datetime.now().year
    return f"{current_year}-12-31"


def load_file_match_config() -> Dict[str, List[str]]:
    """
    加载文件匹配规则配置
    Returns:
        包含答案文件和评分文件匹配规则的字典
    """
    config = configparser.ConfigParser()
    config_file = 'file_match_config.ini'

    # 默认匹配规则
    default_config = {
        'FileMatch': {
            'answer_patterns': '答案,参考答案,standard,answer',
            'scoring_patterns': '评分,打分,分数,score,scoring'
        }
    }

    # 如果配置文件不存在，创建默认配置
    if not os.path.exists(config_file):
        config['FileMatch'] = default_config['FileMatch']
        with open(config_file, 'w', encoding='utf-8') as f:
            config.write(f)
    else:
        config.read(config_file, encoding='utf-8')

    # 获取匹配规则列表
    answer_patterns = [p.strip() for p in config['FileMatch'].get('answer_patterns', '').split(',')]
    scoring_patterns = [p.strip() for p in config['FileMatch'].get('scoring_patterns', '').split(',')]

    return {
        'answer_patterns': answer_patterns,
        'scoring_patterns': scoring_patterns
    }


def find_matching_files(directory: str, patterns: List[str]) -> List[str]:
    """
    在指定目录中查找匹配的文件
    Args:
        directory: 要搜索的目录
        patterns: 匹配模式列表
    Returns:
        匹配的文件路径列表
    """
    matching_files = []
    for file in os.listdir(directory):
        if file.endswith(('.xlsx', '.xls')):
            file_lower = file.lower()
            if any(pattern.lower() in file_lower for pattern in patterns):
                matching_files.append(os.path.join(directory, file))
    return matching_files


def select_file_with_auto_detect(directory: str,
                                 file_type: str,
                                 patterns: List[str]) -> str:
    """
    自动检测并选择文件，如果没有找到或用户不接受建议的文件，则让用户手动选择
    Args:
        directory: 要搜索的目录
        file_type: 文件类型描述（用于显示）
        patterns: 匹配模式列表
    Returns:
        选择的文件路径
    """
    # 查找匹配的文件
    matching_files = find_matching_files(directory, patterns)

    if matching_files:
        # 如果找到匹配的文件，询问用户是否使用
        file_list = "\n".join([os.path.basename(f) for f in matching_files])
        message = f"在目录中找到以下{file_type}文件：\n\n{file_list}\n\n是否使用这个文件？"
        if messagebox.askyesno("确认", message):
            return matching_files[0]
        else:
            # 用户选择不使用自动检测的文件
            messagebox.showinfo(
                "提示",
                f"您选择不使用自动检测的{file_type}文件，\n接下来将打开文件选择对话框，请手动选择{file_type}文件。"
            )
    else:
        # 没有找到匹配的文件
        messagebox.showinfo(
            "提示",
            f"在目录中未找到符合条件的{file_type}文件，\n支持的文件名包含以下关键词：\n{', '.join(patterns)}\n\n"
            f"接下来将打开文件选择对话框，请手动选择{file_type}文件。"
        )

    # 让用户手动选择文件
    selected_file = select_file(f"请选择{file_type}文件")

    # 如果用户取消选择，显示提示
    if not selected_file:
        messagebox.showwarning(
            "警告",
            f"未选择{file_type}文件，程序可能无法继续。"
        )

    return selected_file


def setup_working_environment() -> Tuple[str, str, str, str]:
    """
    设置工作环境，包括选择目录和文件
    Returns:
        (工作目录, 答案文件路径, 评分文件路径, 输出目录路径)
    """
    # 加载文件匹配规则
    match_config = load_file_match_config()

    # 选择工作目录
    work_dir = select_with_guide(
        "请选择工作目录",
        "请选择包含答案文件、评分文件和学生作业的目录。",
        select_directory,
        "选择工作目录"
    )

    if not work_dir:
        raise ValueError("未选择工作目录")

    # 自动检测或选择答案文件
    answer_file = select_file_with_auto_detect(
        work_dir,
        "答案",
        match_config['answer_patterns']
    )

    if not answer_file:
        raise ValueError("未选择答案文件")

    # 自动检测或选择评分文件
    scoring_file = select_file_with_auto_detect(
        work_dir,
        "评分标准",
        match_config['scoring_patterns']
    )

    if not scoring_file:
        raise ValueError("未选择评分文件")

    # 创建结果目录
    output_dir = os.path.join(work_dir, "评分结果")
    os.makedirs(output_dir, exist_ok=True)

    return work_dir, answer_file, scoring_file, output_dir


def main():
    # 检查license
    license_manager = LicenseManager()
    can_use, message = license_manager.check_license()

    if not can_use:
        root = tk.Tk()
        root.withdraw()
        if messagebox.askyesno("License提示", f"{message}\n是否现在激活？"):
            activation_dialog = ActivationDialog(root, license_manager)
            root.wait_window(activation_dialog.window)
            can_use, message = license_manager.check_license()
            if not can_use:
                return
        else:
            return

    scorer = ExcelScorer()

    print("\n=== Excel作业评分工具 ===")
    try:
        # 设置工作环境
        work_dir, answer_file, scoring_file, output_dir = setup_working_environment()

        # 设置评分器的属性
        scorer.standard_file = answer_file
        scorer.scoring_file = scoring_file
        scorer.submissions_dir = work_dir
        scorer.output_dir = output_dir

        # 检查选择的目录中是否有Excel文件
        if not any(f.endswith(('.xlsx', '.xls')) for f in os.listdir(work_dir)):
            messagebox.showerror("错误", f"在工作目录中未找到Excel文件！\n目录：{work_dir}")
            return

        # 获取总分设置
        total_score = create_score_input_window()

        # 显示开始处理的提示
        processing_window = create_guide_window(
            "正在处理文件，请稍候...\n\n"
            "系统将自动比对所有作业并生成评分报告。"
        )
        processing_window.update()

        try:
            # 处理提交的作业，传入总分
            scorer.process_submissions(total_score)
            processing_window.destroy()
            messagebox.showinfo("完成", f"评分完成！\n结果已保存到：{scorer.output_dir}")
        except Exception as e:
            processing_window.destroy()
            messagebox.showerror("错误", f"处理过程中出现错误：\n{str(e)}")

    except ValueError as e:
        messagebox.showerror("错误", str(e))
    except Exception as e:
        messagebox.showerror("错误", f"发生未知错误：\n{str(e)}")


if __name__ == "__main__":
    main()
